프로세스 동기화

@Wonyu· January 03, 2025 · 15 min read

프로세스 간 통신

프로세스 혹은 스레드는 독립적으로 실행된다. 한 프로세스 내 스레드끼리는 같은 메모리 영역을 공유하기 때문에 어렵지 않지만 프로세스 간 통신은 다르다. 서로 다른 프로세스는 메모리 영역이 다르기 때문에 공유된 메모리를 통해 통신하는 것이 쉽지 않다. 운영체제는 프로세스 간 통신 방법을 제공하는데 이를 IPC(Inter Process Communication) 라고 한다.

대표적인 프로세스 간 통신 종류

  • 공유 메모리나 공유 파일을 이용한 통신: 일정한 메모리 영역이나 파일을 공유하고 이를 통해 데이터를 주고 받는다. 데이터를 주고 받는 방법을 프로세스끼리 결정해야 하기 때문에 가장 원시적인 방법이다.
  • 파이프를 이용한 통신: 하나의 컴퓨터 내 프로세스 간 통신에 많이 사용하는 수단이다. 파이프는 프로세스 간 통신을 위해 운영체제가 제공하는 통신 기법이다. 보통 fork()로 만들어진 부모-자식 간 통신에 사용된다.
  • 소켓을 이용한 통신: 네트워크로 연결된 컴퓨터에서 데이터를 주고받기 위한 통신 기법이다. 파이프와 비교했을 때 초기화할 내용도 많고 시스템 자원도 많이 사용하기 때문에 같은 컴퓨터 내 통신에선 비효율적이다.

통신 방향에 따른 분류

  • 양방향 통신: 데이터를 양쪽 방향으로 동시에 전송할 수 있는 구조 ex) 소켓 이용 통신, 전화
  • 반양방향 통신: 데이터를 양쪽 방향으로 전송 가능하지만, 동시 전송은 불가능한 구조 ex) 무전기
  • 단방향 통신: 한쪽 방향으로만 데이터를 전송할 수 있는 구조 ex) 파이프 이용 통신

통신 구현 방식에 따른 분류

공유 메모리를 사용하는 통신 방식의 가장 큰 문제는 상대방이 데이터를 언제 보낼지 받는 쪽에서 모른다는 것이다. 따라서 데이터를 받는 쪽에서 반복적으로 공유 메모리를 점검하는 수밖에 없다. 이렇게 수시로 점검하며 기다리는 상태를 busy waiting이라고 한다. busy waiting은 시스템 자원 낭비가 크다. 이를 해결하기 위해 데이터가 도착했음을 알려주는 동기화를 사용한다.

프로세스 간 통신은 동기화 기능 여부에 따라 대기가 있는 통신과 없는 통신으로 구분된다. 대기가 있는 통신(blocking communication)을 동기화 통신(synchronous communication), 대기가 없는 통신(non-blocking communication)을 비동기화 통신(asynchronous communication)이라고 한다.

파이프를 이용한 통신과 소켓을 이용한 통신은 대기가 있는 통신이므로 바쁜 대기를 사용하지 않고도 통신이 가능하다.

파이프를 이용한 통신

pipe 만약 프로세스 B가 파이프1에 대해 읽기 연산을 수행했는데 프로세스 A가 파이프 1에 대해 쓰기 작업 중이라면, 프로세스 B는 대기 상태가 된다. 이러한 대기 상태는 프로세스 A의 쓰기 작업이 끝나는 순간 자동으로 풀려 동기화가 이뤄진다. 따라서 프로세스 B는 busy waiting를 하지 않아도 된다.

위처럼 파이프 통신 방식은 단방향 통신이기 때문에 양방향 통신을 하기 위해선 2개의 파이프가 필요하다.

소켓을 이용한 통신

서버 쪽 웹 데몬(HTTPD) 포트에 여러 클라이언트가 요청을 보냈다고 해보자. 만약 소켓을 사용하지 않는다면 한 클라이언트가 포트를 장악하고 나머지 클라이언트들은 하염없이 기다려야 한다. 하나의 포트에 여러 클라이언트를 연결하려면 소켓이 필요하다. 클라이언트들은 포트에 연결된 멀티 소켓에 하나씩 연결된다.

네트워킹 상황에서 통신할 때는 연결 프로시저 호출이나 소켓을 이용한다. 원격 프로시저 호출은 다른 컴퓨터에 있는 함수를 호출하는 것을 말한다. 이는 일반적으로 소켓으로 구현된다.

소켓을 이용한 통신에서는 소켓 1개만 사용해도 양방향 통신이 가능하다.


공유 자원과 임계구역

공유 자원

정의:

  • 여러 프로세스나 스레드가 동시에 접근하거나 사용할 수 있는 자원

    ex) 메모리, 파일, 데이터베이스, 프린터 등

문제점: 여러 프로세스 또는 스레드가 동시에 공유 자원에 접근하면 데이터 충돌, 불일치, 손상 등의 문제가 발생할 수 있다.

임계구역

정의:

  • 공유 자원에 접근하는 코드 영역으로, 하나의 프로세스나 스레드만이 진입 가능해야 한다.
  • 임계 구역은 공유 자원을 안전하게 사용하기 위해 동기화 기법을 필요로 한다.

경쟁 조건(race condition)

여러 프로세스나 스레드가 동시에 공유 자원에 접근하면서, 실행 결과가 접근 순서에 따라 달라지는 상태

생산자-소비자 문제

생산자-소비자 문제는 Race condition의 전통적인 예시다. 생산자는 물건의 수를 증가시키고, 소비자는 물건의 수를 감소시킨다. 만약 생산자 코드와 소비자 코드가 동시에 실행될 경우 문제가 발생한다. 현재 총합(sum)이 3이라고 해보자.

생산자 코드 먼저 실행

생산자 sum = 4
소비자 sum = 2
결과 sum = 2

소비자 코드 먼저 실행

소비자 sum = 2
생산자 sum = 4
결과 sum = 4

임계구역 문제 해결 조건

임계구역 문제를 해결하는 데 있어 가장 중요한 조건은 다음과 같다.

상호 배제(mutual exclusion): 한 번에 하나의 프로세스만 임계 구역에 진입할 수 있도록 보장

한정 대기(bounded waiting): 어떤 프로세스도 임계구역에 진입하지 못해 무한 대기하지 않아야 함

진행의 융통성(progress flexibility): 한 프로세스가 다른 프로세스의 진행을 방해해서는 안 됨


임계구역 문제 해결 방법

문제 예시

  • 동시 진입 상황: 상호 배제 조건이 충족되지 않아 두 프로세스 이상이 임계구역에 접근
  • 한정 대기 문제: 락을 풀지 못해 작업이 진행되지 못하는 Deadlock 발생
  • 진행의 융통성 문제: 한 프로세스의 진행이 다른 프로세스로 인해 방해받는다.

세마포어

공유 자원의 접근을 제어하기 위한 동기화 도구로 자원을 사용할 수 있는 상태를 나타내는 정수 값을 유지하며, 프로세스나 스레드가 자원에 접근할 때 값을 변경하여 동기화를 구현한다

  • 구성

      1. 정수 값 (S):
      2. 자원의 상태를 나타낸다
      3. 초기 값은 사용할 수 있는 자원의 개수로 설정된다
    • 두 가지 기본 연산:

      • wait() (또는 P()): 자원을 요청

        • 자원을 사용할 수 있으면 값을 감소시키고, 사용 권한을 부여
        • 자원을 사용할 수 없으면 대기
      • signal() (또는 V()): 자원을 해제

        • 자원의 사용이 끝났음을 알리고, 값을 증가시킴
        • 대기 중인 프로세스가 있으면, 자원 사용 권한 부여 wait() 연산 자원을 요청하며 다음과 같이 작동한다
  • 세마포어 값 S가 1 이상이면:

    • S를 1 감소시킨다
    • 요청 프로세스는 자원을 사용한다
  • 세마포어 값 S가 0이면:

    • 요청 프로세스는 대기 큐에 들어가며, 자원 사용 대기 signal() 연산 자원을 해제하며 다음과 같이 작동합니다:
  • 세마포어 값 S가 대기 중인 프로세스가 없다면:

    • S를 1 증가시킨다
  • 세마포어 값 S가 대기 중인 프로세스가 있다면:

    • 대기 중인 프로세스 하나를 깨워 자원을 사용하게 한다

다음은 2개의 공유 자원에 대해 3개의 프로세스가 작업하는 상황을 나타낸다. semaphore 문제점

  • 복잡성:

    • 다수의 프로세스가 세마포어를 사용할 경우 설계가 어려움
  • 데드락(Deadlock):

    • 프로세스 간의 자원 요청 순서가 꼬일 경우, 데드락이 발생할 수 있음
  • 기아 상태(Starvation):

    • 우선순위가 낮은 프로세스가 계속 대기하게 될 수 있음
  • 휴먼 에러:

    • 프로그래머가 직접 구현해야 하므로 이를 잘못 사용하거나 사용하지 않을 수 있음

모니터

프로세스나 스레드가 공유 자원에 안전하게 접근할 수 있도록 동기화와 상호 배제를 보장하는 고수준의 동기화 도구공유 자원과 이를 사용하는 코드(임계 구역)를 캡슐화하여 동시성 문제를 해결한다.

위 세마포어 알고리즘이 모든 프로세스에서 자동으로 처리된다면, 굳이 따로 wait(), signal()를 사용할 필요가 없다. 모니터는 이를 실제로 구현한 것으로 공유 자원을 내부적으로 숨기고 접근하기 위한 인터페이스만 제공한다.

모니터는 시스템 자원을 사용자로부터 숨기고 사용자 요구 사항을 처리할 수 있는 인터페이스만을 제공한다는 점에서 system call과 같은 개념이라고 볼 수 있다. 장점

  1. 캡슐화:

    • 공유 자원과 연산을 하나의 단위로 관리하여 코드 가독성과 유지보수성 향상
  2. 안전성:

    • 동기화 오류 가능성 감소

궁금했던 부분

non-blocking communication의 발전

위에서 언급한 대로 non-blocking communication의 busy waiting을 blocking communication이 동기화를 통해 해결했다. 하지만 동기화 방식은 대기 상태가 길어질 경우 전체 응답성이 떨어지고 성능이 저하될 수 있다는 단점을 가지고 있다.

현대 non-blocking communication은 busy waiting과 동기화 방식의 대기 상태 문제를 해결한다.

  • 고안된 방식:

    • 데이터를 수신할 준비가 되었는지 상태를 반복적으로 확인하지 않고, 이벤트 기반 처리콜백 메커니즘을 사용
    • CPU는 데이터 송수신을 기다리지 않고 다른 작업을 계속 수행
  • 동작 원리:

    1. 이벤트 기반 처리:

      • 데이터가 준비되면 이벤트를 발생시켜 관련 작업을 처리
    2. 콜백 메커니즘:

      • 데이터 송수신이 완료되었을 때, 사전에 등록된 콜백 함수 호출
  • 장점:

    • 응답 시간을 개선하고, 다수의 작업을 동시에 처리 가능
    • CPU 자원을 최대한 활용하여 병렬성을 높임

원격 프로시저 호출(RPC)

개념

  • 원격 프로시저 호출(RPC)은 네트워크 상의 다른 시스템(서버)에 있는 함수를 로컬 함수처럼 호출할 수 있게 해주는 기술 예시

Server

from xmlrpc.server import SimpleXMLRPCServer

def add(a, b):
    return a + b

# 서버 생성
server = SimpleXMLRPCServer(("localhost", 8080))
print("Server is running on port 8080...")
server.register_function(add, "add")  # 함수 등록
server.serve_forever()

Client

import xmlrpc.client

# 서버 연결
client = xmlrpc.client.ServerProxy("http://localhost:8080")

# 원격 함수 호출
result = client.add(5, 3)
print(f"Result from server: {result}")
@Wonyu
Hello :) I'm Wonyu